package flare.data.converters
{
  import flare.data.DataField;
  import flare.data.DataSchema;
  import flare.data.DataSet;
  import flare.data.DataTable;
  import flare.data.DataUtil;

  import flash.utils.ByteArray;
  import flash.utils.IDataInput;
  import flash.utils.IDataOutput;

  /**
   * Converts data between delimited text (e.g., tab delimited) and
   * flare DataSet instances.
   */
  public class DelimitedTextConverter implements IDataConverter
  {
    private var _delim:String;

    public function get delimiter():String
    {
      return _delim;
    }

    public function set delimiter(d:String):void
    {
      _delim = d;
    }

    /**
     * Creates a new DelimitedTextConverter.
     * @param delim the delimiter string separating values (tab by default)
     */
    public function DelimitedTextConverter(delim:String = "\t")
    {
      _delim = delim;
    }

    /**
     * @inheritDoc
     */
    public function read(input:IDataInput, schema:DataSchema = null):DataSet
    {
      return parse(input.readUTFBytes(input.bytesAvailable), schema);
    }

    /**
     * Converts data from a tab-delimited string into a DataSet object.
     * @param text the loaded input data
     * @param schema a data schema describing the structure of the data.
     *  Schemas are optional in many but not all cases.
     * @return a DataSet of converted data objects.
     */
    public function parse(text:String, schema:DataSchema = null):DataSet
    {
      var tuples:Vector.<Object> = new Vector.<Object>();
      var lines:Array = text.split(/\r\n|\r|\n/);

      if(schema == null)
      {
        schema = inferSchema(lines);
      }

      var i:int = schema.hasHeader ? 1 : 0;

      for(;i < lines.length; ++i)
      {
        var line:String = lines[i];

        if(line.length == 0)break;

        var tok:Array = line.split(_delim);
        var tuple:Object = {
        };

        for(var j:int = 0; j < schema.numFields; ++j)
        {
          var field:DataField = schema.getFieldAt(j);
          tuple[field.name] = DataUtil.parseValue(tok[j], field.type);
        }
        tuples.push(tuple);
      }
      return new DataSet(new DataTable(tuples, schema));
    }

    /**
     * @inheritDoc
     */
    public function write(data:DataSet, output:IDataOutput = null):IDataOutput
    {
      if(output == null)output = new ByteArray();
      var tuples:Vector.<Object> = data.nodes.data;
      var schema:DataSchema = data.nodes.schema;

      for each(var tuple:Object in tuples)
      {
        var i:int = 0, s:String;

        if(schema == null)
        {
          for(var name:String in tuple)
          {
            if(i > 0)output.writeUTFBytes(_delim);
            output.writeUTFBytes(String(tuple[name])); // TODO: proper string formatting
            ++i;
          }
        }

        else
        {
          for(;i < schema.numFields; ++i)
          {
            var f:DataField = schema.getFieldAt(i);

            if(i > 0)output.writeUTFBytes(_delim);
            output.writeUTFBytes(String(tuple[f.name])); // TODO proper string formatting
          }
        }
        output.writeUTFBytes("\n");
      }
      return output;
    }

    /**
     * Infers the data schema by checking values of the input data.
     * @param lines an array of lines of input text
     * @return the inferred schema
     */
    // Not changing to Array as a common parameter is the output from
    // the String.split function, which returns an Array.
    protected function inferSchema(lines:Array):DataSchema
    {
      var header:Array = lines[0].split(_delim);
      var types:Array = new Array(header.length);

      // initialize data types
      var tok:Array = lines[1].split(_delim);

      for(var col:int = 0; col < header.length; ++col)
      {
        types[col] = DataUtil.type(tok[col]);
      }

      // now process data to infer types
      for(var i:int = 2; i < lines.length; ++i)
      {
        tok = lines[i].split(_delim);

        for(col = 0; col < tok.length; ++col)
        {
          if(types[col] == -1)continue;
          var type:int = DataUtil.type(tok[col]);

          if(types[col] != type)
          {
            types[col] = -1;
          }
        }
      }

      // finally, we create the schema
      var schema:DataSchema = new DataSchema();
      schema.hasHeader = true;

      for(col = 0; col < header.length; ++col)
      {
        schema.addField(new DataField(header[col], types[col] == -1 ? DataUtil.STRING : types[col]));
      }
      return schema;
    }
  } // end of class DelimitedTextConverter
}